import {
  CXXFile,
  CXXTYPE,
  CXXTerraNode,
  Clazz,
  SimpleTypeKind,
} from "@agoraio-extensions/cxx-parser";
import {
  ParseResult,
  RenderResult,
  TerraContext,
} from "@agoraio-extensions/terra-core";
import { isCallbackClass } from "./utils";
import { dartFileName, dartName } from "../parsers/dart_syntax_parser";
import { getIrisApiIdValue } from "@agoraio-extensions/terra_shared_configs";
import { ApiInterfaceRendererArgs } from "./api_interface_renderer";

import path from "path";

function processCXXFiles(
  terraContext: TerraContext,
  parseResult: ParseResult,
  args: any
): CXXFile[] {
  return (parseResult.nodes as CXXFile[]).filter((cxxFile) => {
    return cxxFile.nodes.find((node) => {
      return node.__TYPE == CXXTYPE.Clazz && isCallbackClass((node as Clazz).name);
    });
  });
}

function fileterNodes(cxxFile: CXXFile): CXXTerraNode[] {
  return cxxFile.nodes
    .filter((it) => it.__TYPE == CXXTYPE.Clazz)
    .filter((it) => isCallbackClass(it.asClazz().name));
}

const dartHeader = `
/// GENERATED BY terra, DO NOT MODIFY BY HAND.\n\n// ignore_for_file: public_member_api_docs, unused_local_variable, unused_import

import 'binding_forward_export.dart';
// import 'package:agora_rtc_engine/src/binding/impl_forward_export.dart';
import 'package:iris_method_channel/iris_method_channel.dart';
`.trim();

/// TODO(littlegnal): Move to a parser.
const filteredBaseClasses = ["RefCountInterface", "agora::base::IEngineBase"];
function getBaseClasses(parseResult: ParseResult, clazz: Clazz): Clazz[] {
  let output: Clazz[] = [];
  clazz.base_clazzs.forEach((it) => {
    if (!filteredBaseClasses.includes(it)) {
      let found = parseResult.resolveNodeByName(it);
      if (found) {
        output.push(found! as Clazz);
      }
    }
  });

  return output;
}

function genCallbackExtendBlock(parseResult: ParseResult, clazz: Clazz) {
  let extendBlock = "";
  let wrapperClassName = getBaseClasses(parseResult, clazz).map(
    (it) => `${dartName(it)}Wrapper`
  );
  if (wrapperClassName.length === 0) {
    extendBlock = `implements EventLoopEventHandler`;
  } else {
    // Only take first one.
    extendBlock = `extends ${wrapperClassName[0]}`;
  }

  return extendBlock;
}

function callbackSwithCaseBlock(
  parseResult: ParseResult,
  clazz: Clazz,
  firstParamNameForWrapperClass: string
): string {
  return clazz.methods
    .map((it) => {
      let className = clazz.name;
      let methodName = it.name;
      let jsonClassName = `${className.replace("I", "")}${
        methodName[0].toUpperCase() + methodName.slice(1)
      }Json`;
      let dn = dartName(it);
      let eventName = getIrisApiIdValue(it).split("_").slice(1).join("_");
      // Fall back to method name if `getIrisApiIdValue` is empty. This is happen for that nodes from custom headers.
      if (eventName === "") {
        eventName = it.name;
      }

      return `
case '${eventName}':
if (${firstParamNameForWrapperClass}.${dn} == null) {
    return true;
}
final jsonMap = jsonDecode(eventData);
${jsonClassName} paramJson = ${jsonClassName}.fromJson(jsonMap);
paramJson = paramJson.fillBuffers(buffers);
${(function () {
  let paramIntList = it.parameters
    .map((it) => {
      let typeName = dartName(it.type);
      let memberName = dartName(it);
      return `${typeName}? ${memberName} = paramJson.${memberName};`;
    })
    .join("\n");

  let paramNullCheckList = it.parameters
    .map((it) => {
      let memberName = dartName(it);
      return `${memberName} == null`;
    })
    .join("||");
  if (paramNullCheckList.length) {
    paramNullCheckList = `if (${paramNullCheckList}) { return true; }`;
  }

  let paramFillBufferList = it.parameters
    .map((it) => {
      let memberName = dartName(it);
      let actualNode = parseResult.resolveNodeByType(it.type);
      if (actualNode.__TYPE == CXXTYPE.TypeAlias) {
        actualNode = parseResult.resolveNodeByType(
          actualNode.asTypeAlias()!.underlyingType
        );
      }
      if (actualNode.__TYPE == CXXTYPE.Struct) {
        if (it.type.kind == SimpleTypeKind.array_t) {
          let bufferCount =
            actualNode.asStruct()!.member_variables.filter(
              (m) => dartName(m.type) == "Uint8List"
            ).length;

          if (bufferCount <= 1) {
            return `${memberName} = ${memberName}.asMap().entries.map((entry) => entry.value.fillBuffers([if (entry.key < buffers.length) buffers[entry.key]])).toList();`;
          } else {
            let listArgs = new Array(bufferCount)
              .fill(0)
              .map(
                (_: any, idx: number) =>
                  `if (entry.key * ${bufferCount} + ${idx} < buffers.length) buffers[entry.key * ${bufferCount} + ${idx}]`
              )
              .join(", ");
            return `${memberName} = ${memberName}.asMap().entries.map((entry) => entry.value.fillBuffers([${listArgs}])).toList();`;
          }
        } else {
          return `${memberName} = ${memberName}.fillBuffers(buffers);`;
        }
      }

      return "";
    })
    .join("\n")
    .trim();

  let paramList = it.parameters.map((it) => dartName(it));

  return `
  ${paramIntList}
  ${paramNullCheckList}
  ${paramFillBufferList}
  ${firstParamNameForWrapperClass}.${methodName}!(${paramList});
  return true;
  `.trim();
})()}
`;
    })
    .join("\n");
}

export default function EventHandlersImplRenderer(
  terraContext: TerraContext,
  args: ApiInterfaceRendererArgs,
  parseResult: ParseResult
): RenderResult[] {
  let outputDir = args.outputDir ?? "";
  let cxxFiles = processCXXFiles(terraContext, parseResult, args);

  return cxxFiles.map((cxxFile) => {
    let output = fileterNodes(cxxFile)
      .map((it) => it.asClazz())
      .map((it) => {
        let clazz = it;
        let className = dartName(clazz);
        let extendBlock = genCallbackExtendBlock(parseResult, clazz);
        let wrapperClassName = `${className}Wrapper`;
        let hasBaseClass = clazz.base_clazzs.length > 0;
        let firstParamNameForWrapperClass =
          className[0].toLowerCase() + className.slice(1);

        return `
class ${wrapperClassName} ${extendBlock} {
${(function () {
  // Constructor
  let callSuperBlock = hasBaseClass
    ? `: super(${firstParamNameForWrapperClass})`
    : "";

  return `
    const ${wrapperClassName}(this.${firstParamNameForWrapperClass})${callSuperBlock};
    `.trim();
})()}

final ${className} ${firstParamNameForWrapperClass};

@override
bool operator ==(Object other) {
  if (other.runtimeType != runtimeType) {
    return false;
  }
  return other is ${wrapperClassName} &&
      other.${firstParamNameForWrapperClass} == ${firstParamNameForWrapperClass};
}
@override
int get hashCode => ${firstParamNameForWrapperClass}.hashCode;

@override
bool handleEventInternal(String eventName, String eventData, List<Uint8List> buffers) {
    switch (eventName) {
        ${callbackSwithCaseBlock(
          parseResult,
          clazz,
          firstParamNameForWrapperClass
        )}
    }
    return false;
}

@override
bool handleEvent(String eventName, String eventData, List<Uint8List> buffers) {
    if (!eventName.startsWith('${className}')) return false;
    final newEvent = eventName.replaceFirst('${className}_', '');
    if (handleEventInternal(newEvent, eventData, buffers)) { return true; }
    ${(function () {
      // handle return block
      if (hasBaseClass) {
        return "return super.handleEventInternal(newEvent, eventData, buffers);";
      }

      return "return false;";
    })()}
}
}
`.trim();
      })
      .join("\n");

    let content = `
    ${dartHeader}
  
    ${output}
    `.trim();

    let fileName = `${dartFileName(cxxFile)}_event_impl.dart`;
    return {
      file_name: path.join(outputDir, fileName),
      file_content: content,
    };
  });
}
